package org.am.ether;

import org.am.ether.bean.Eip20Data;
import org.am.ether.bean.EtherWallet;
import org.am.ether.ext.AbiTypeDecoder;
import org.am.ether.ext.EtherWeb3jExceptioin;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Address;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.Type;
import org.web3j.abi.datatypes.Utf8String;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.abi.datatypes.generated.Uint8;
import org.web3j.crypto.*;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.response.*;
import org.web3j.protocol.http.HttpService;
import org.web3j.utils.Numeric;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class EtherWeb3j {
    private BigInteger ethWei = new BigInteger("1000000000000000000");
    private String rpcUrl;
    private Web3j web3j;
    private BigInteger gasLimit = BigInteger.valueOf(90000L);
    private static final String METHOD_BALANCEOF = "balanceOf";
    private static final String METHOD_TRANSFER = "transfer";
    private static final String METHOD_SYMBOL = "symbol";
    private static final String METHOD_DECIMALS = "decimals";

    private void init() {
        web3j = Web3j.build(new HttpService(this.rpcUrl));
    }

    public EtherWeb3j(String rpcUrl) {
        this.rpcUrl = rpcUrl;
        init();
    }

    public EtherWeb3j(String rpcUrl, BigInteger gasLimit) {
        this.rpcUrl = rpcUrl;
        this.gasLimit = gasLimit;
        init();
    }

    public EtherWallet genWalletByKey(byte[] privateKey) {
        try {
            ECKeyPair ecKeyPair = ECKeyPair.create(privateKey);
            return buildWallet(ecKeyPair);
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }


    public EtherWallet genWallet() {
        return genWallet(null);
    }

    public EtherWallet genWallet(String secret) {
        try {
            ECKeyPair ecKeyPair;
            if (secret == null) {
                ecKeyPair = Keys.createEcKeyPair();
            } else {
                ecKeyPair = ECKeyPair.create(new BigInteger(secret, 16));
            }
            return buildWallet(ecKeyPair);
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    private EtherWallet buildWallet(ECKeyPair ecKeyPair) throws Exception {
        WalletFile wallet = Wallet.createLight("", ecKeyPair);
        String walletAddress = wallet.getAddress();
        String walletSecret = ecKeyPair.getPrivateKey().toString(16);
        return new EtherWallet("0x" + walletAddress, walletSecret);
    }

    public BigInteger getBalance(String address) {
        return getBalance(address, null);
    }

    public BigInteger getBalance(String address, String contract) {
        try {
            if (contract == null) {
                EthGetBalance ethGetBalance = web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send();
                return ethGetBalance.getBalance();
            } else {
                Function function = new Function(METHOD_BALANCEOF, Collections.singletonList(new Address(address)),
                        Collections.singletonList(new TypeReference<Uint256>() {
                        }));
                Type result = execEthCall(function, address, contract);
                if (result == null) {
                    return null;
                }
                return new BigInteger(result.getValue().toString());
            }
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    public Long getBlockNumber() {
        try {
            EthBlockNumber ethBlockNumber = web3j.ethBlockNumber().send();
            BigInteger intBlockNumber = ethBlockNumber.getBlockNumber();
            return intBlockNumber.longValue();
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    public EthSendTransaction sendTransaction(String secret, String from, String to, BigInteger value) {
        return sendTransaction(secret, from, to, value, null);
    }

    public EthSendTransaction sendTransaction(String secret, String from, String to, BigInteger value, String contract) {
        try {
            BigInteger nonce = getNonce(from);
            BigInteger gas = getGas();
            String txBlob = buildRawTransaction(secret, to, nonce, gas, value, contract);
            return web3j.ethSendRawTransaction(txBlob).send();
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    public org.web3j.protocol.core.methods.response.Transaction getTransactionByHash(String hash) {
        try {
            EthTransaction tx = web3j.ethGetTransactionByHash(hash).send();
            return tx.getTransaction().orElse(null);
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    public BigInteger getNonce(String from) {
        try {
            EthGetTransactionCount transactionCount = web3j.ethGetTransactionCount(from, DefaultBlockParameterName.LATEST).send();
            return transactionCount.getTransactionCount();
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    public BigInteger getGas() {
        try {
            EthGasPrice ethGasPrice = web3j.ethGasPrice().send();
            return ethGasPrice.getGasPrice();
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    public String getEip20Symbol(String contract) {
        try {
            Function function = new Function(METHOD_SYMBOL, Collections.emptyList(), Collections.singletonList(new TypeReference<Utf8String>() {
            }));
            Type result = execEthCall(function, contract, contract);
            if (result == null) {
                return null;
            }
            return result.toString();
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    public int getEip20Decimals(String contract) {
        try {
            Function function = new Function(METHOD_DECIMALS, Collections.emptyList(), Collections.singletonList(new TypeReference<Uint8>() {
            }));
            Type result = execEthCall(function, contract, contract);
            if (result == null) {
                return 0;
            }
            return new BigInteger(result.getValue().toString()).intValue();
        } catch (Exception e) {
            throw new EtherWeb3jExceptioin(e);
        }
    }

    public String buildRawTransaction(String privateKey, String to, BigInteger nonce, BigInteger gas, BigInteger value, String contract) {
        RawTransaction rawTransaction;
        if (contract == null) {
            rawTransaction = RawTransaction.createEtherTransaction(nonce, gas, gasLimit, to, value);
        } else {
            Function function = new Function(METHOD_TRANSFER, Arrays.asList(new Address(to), new Uint256(value)), Collections.singletonList(new TypeReference<Type>() {
            }));
            String data = FunctionEncoder.encode(function);
            rawTransaction = RawTransaction.createTransaction(nonce, gas, gasLimit, contract, data);
        }
        Credentials credentials = Credentials.create(privateKey);
        byte[] signMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
        return Numeric.toHexString(signMessage);
    }

    public Eip20Data parseEip20Input(String inputData) {
        String methodId = inputData.substring(0, 10);
        String toStr = inputData.substring(10, 74);
        String valueStr = inputData.substring(74);

        Address address = AbiTypeDecoder.decode(toStr, 0, Address.class);
        Uint256 amount = AbiTypeDecoder.decode(valueStr, 0, Uint256.class);
        return new Eip20Data(methodId, address, amount);
    }

    private Type execEthCall(Function function, String fromAddress, String toAddress) throws Exception {
        String encode = FunctionEncoder.encode(function);
        org.web3j.protocol.core.methods.request.Transaction ethCallTransaction = org.web3j.protocol.core.methods.request.Transaction.createEthCallTransaction(fromAddress, toAddress, encode);
        EthCall ethCall = web3j.ethCall(ethCallTransaction, DefaultBlockParameterName.LATEST).send();
        List<Type> values = FunctionReturnDecoder.decode(ethCall.getValue(), function.getOutputParameters());
        if (!values.isEmpty()) {
            return values.get(0);
        } else {
            return null;
        }
    }

    public Web3j getWeb3j() {
        return web3j;
    }
}
